package essential;

import helpers.ColorHelper;

import java.awt.Color;
import java.awt.image.BufferedImage;

import javax.vecmath.Vector3d;

import primitives.Primitive;
import primitives.Renderable;

public class Render extends Thread {

	public final float EPSILON = 0.0000000001f;
	public final int MAX_DEPTH = 3;
	// Funciona mejor con 15 para ambas computadoras
	public double far = 0;
	
	// renderer data
	static Scene scene;
	static Camera camera;
	static BufferedImage m_Dest;
	static int m_Width, m_Height;
	static boolean antialiasing;
	static int antiAliasingMinDepth;
	static int antiAliasingMaxDepth;
	final static int antialiasingMaxThereshold = 255;
	final static float antialiasingThereshold = 0.50f * antialiasingMaxThereshold;

	boolean logCV;

	int yFrom, yTo, xFrom, xTo;
	private int colors[];

	public Render(int xFrom, int xTo, int yFrom, int yTo) {

		this.yFrom = yFrom;
		this.yTo = yTo;
		this.xFrom = xFrom;
		this.xTo = xTo;
		colors = new int[(xTo - xFrom) * (yTo - yFrom)];
	}

	public void run() {

		// render scene
		// Get the origin
		Vector3d o = new Vector3d(camera.getOrigin());

		// Calculate the step vectors
		Vector3d stepW = new Vector3d(camera.rightUpCorner());
		stepW.sub(camera.leftUpCorner());
		stepW.scale(1 / ((double) m_Width));

		Vector3d stepH = new Vector3d(camera.leftDownCorner());
		stepH.sub(camera.leftUpCorner());
		stepH.scale(1 / ((double) m_Height));

		// Set the accumulative height vector
		Vector3d accumH = new Vector3d(camera.leftUpCorner());

		if (yFrom > 0) {
			Vector3d aux = new Vector3d(stepH);
			aux.scale(yFrom);
			accumH.add(aux);
		}

		// render remaining lines
		Ray r = new Ray(o, null);
		Vector3d dir = new Vector3d();
		Vector3d accumW = new Vector3d();

		if (xFrom > 0) {
			Vector3d aux = new Vector3d(stepW);
			aux.scale(xFrom);
			accumW.add(aux);
		}

		int prevYColors[] = new int[m_Width + 1];
		int prevXColor = 0;
		
		xTo  += antialiasing? 1 : 0;
		yTo  += antialiasing? 1 : 0;
		
		for (int y = yFrom; y < yTo; y++) {
			// Set the accumulative width vector
			accumW.set(accumH);
			// m_SX = m_WX1;
			// render pixels for current line
			for (int x = xFrom; x < xTo; x++) {
				// fire primary ray
				dir.set(accumW);
				dir.normalize();
				cleanRay(r, dir);

				Color finalColor;
				Color c = getColor(r, 0);

				if (antialiasing) {
					if (x > xFrom && y > yFrom) {
						finalColor = doAntialiasing(r,
						 new Color(prevYColors[x - 1]),
						 new Color(prevYColors[x]),
						 new Color(prevXColor),
						 c, stepW, stepH);
	
						prevYColors[x - 1] = prevXColor;	//update [x - 1] to be used in next line
						prevXColor = ColorToInt(c);
						
//						colors[(y - 1 - yFrom) * (xTo - xFrom) + (x - 1 - xFrom)] = 
//							ColorToInt(finalColor);
						if(RayCaster.MOTION_BLUR){
							m_Dest.setRGB(x - 1, m_Height - y,ColorToInt(ColorHelper.sumColor(
									ColorHelper.mulColor(finalColor,(1.0/RayCaster.motionBlurDt)),new Color(m_Dest.getRGB(x-1,m_Height - y)))));
						}else{
							m_Dest.setRGB(x - 1, m_Height - y, ColorToInt(finalColor));
						}
						
					} else if (y == yFrom) {
						prevYColors[x] = ColorToInt(c);
					} else if (x == xFrom) {
						prevYColors[xTo - 1] = prevXColor;
						prevXColor = ColorToInt(c);
					}
				} else {  
					if(RayCaster.MOTION_BLUR){
						m_Dest.setRGB(x, m_Height - 1 - y,ColorToInt(ColorHelper.sumColor(
								ColorHelper.mulColor(c,(1.0/RayCaster.motionBlurDt)),new Color(m_Dest.getRGB(x,m_Height -1 - y) ))) );
					}else{
						m_Dest.setRGB(x, m_Height - 1 - y, ColorToInt(c));
					}					
				}

				accumW.add(stepW);
				// m_SX += m_DX;
			}
			accumH.add(stepH);
			// m_SY += m_DY;
			RayCaster.IncreaseProgress();

		}
		// all done
	}

	void flushToBufferImage(BufferedImage bi) {
		bi.setRGB(xFrom, yFrom, xTo - xFrom, yTo - yFrom, colors, 0, xTo- xFrom);
	}
	
	
	int ColorToInt(Color c) {
		return (c.getRed() << 16) + (c.getGreen() << 8) + c.getBlue();
	}

	private void cleanRay(Ray r, Vector3d dir) {
		r.setDirection(dir);
		r.setImpactDist(Double.MAX_VALUE);
		r.setLastRenderable(null);
		r.impactColor = null;
		r.impactNormal = null;
	}

	public Color colorMedian(Color c0, Color c1, Color c2, Color c3) {
		return new Color(
				(c0.getRed() + c1.getRed() + c2.getRed() + c3.getRed()) / 4,
				(c0.getGreen() + c1.getGreen() + c2.getGreen() + c3.getGreen()) / 4,
				(c0.getBlue() + c1.getBlue() + c2.getBlue() + c3.getBlue()) / 4);
	}

	public float colorDistance(Color c0, Color c1, Color c2, Color c3) {

		int minR = Math.min(Math.min(c0.getRed(), c1.getRed()), Math.min(c2
				.getRed(), c3.getRed()));
		int minG = Math.min(Math.min(c0.getGreen(), c1.getGreen()), Math.min(c2
				.getGreen(), c3.getGreen()));
		int minB = Math.min(Math.min(c0.getBlue(), c1.getBlue()), Math.min(c2
				.getBlue(), c3.getBlue()));

		int maxR = Math.max(Math.max(c0.getRed(), c1.getRed()), Math.max(c2
				.getRed(), c3.getRed()));
		int maxG = Math.max(Math.max(c0.getGreen(), c1.getGreen()), Math.max(c2
				.getGreen(), c3.getGreen()));
		int maxB = Math.max(Math.max(c0.getBlue(), c1.getBlue()), Math.max(c2
				.getBlue(), c3.getBlue()));

		return Math.max(maxR - minR, Math.max(maxG - minG, maxB - minB));
	}

	public Color doAntialiasing(Ray r, Color c0, Color c1, Color c2, Color c3,
			Vector3d offsetW, Vector3d offsetH) {
		//System.out.println(colorDistance(c0, c1, c2, c3) + " vs " + antialiasingThereshold);
		
		if (colorDistance(c0, c1, c2, c3) > antialiasingThereshold) {
			//System.out.println(colorDistance(c0, c1, c2, c3) + " vs " + antialiasingThereshold);
			return doAntialiasingRecursive(r, c0, c1, c2, c3, offsetW, offsetH, 0);
		}
		return colorMedian(c0, c1, c2, c3);
	}
	
	public Color doAntialiasingRecursive(Ray r, Color c0, Color c1, Color c2, Color c3,
			Vector3d offsetW, Vector3d offsetH, int depth) {
		if ((colorDistance(c0, c1, c2, c3) < antialiasingThereshold
				|| depth < antiAliasingMinDepth)
				&& depth < antiAliasingMaxDepth) {
			//System.out.println("Depth = " + depth);

			// Dir starts in bottom right
			Vector3d backupDir = new Vector3d(r.getDirection());

			Vector3d halfOffsetW = new Vector3d(offsetW);
			halfOffsetW.scale(0.5);

			Vector3d halfOffsetH = new Vector3d(offsetH);
			halfOffsetH.scale(0.5);

			Color bottom = getColor(offsetRay(r, -1, 0, halfOffsetW, halfOffsetH), 0);
			Color left = getColor(offsetRay(r, -1, -1, halfOffsetW, halfOffsetH), 0);
			Color top = getColor(offsetRay(r, 1, -1, halfOffsetW, halfOffsetH), 0);
			Color right = getColor(offsetRay(r, 1, 1, halfOffsetW, halfOffsetH), 0);
			Color middle = getColor(offsetRay(r, -1, 0, halfOffsetW, halfOffsetH), 0);

			// Top left
			c0 = doAntialiasingRecursive(r, c0, top, left, middle, halfOffsetW,
					halfOffsetH, depth+1);

			// Top right
			offsetRay(r, 1, 0, halfOffsetW, halfOffsetH);
			c1 = doAntialiasingRecursive(r, top, c1, middle, right, halfOffsetW,
					halfOffsetH, depth+1);

			// Bottom left
			offsetRay(r, -1, -1, halfOffsetW, halfOffsetH);
			c2 = doAntialiasingRecursive(r, left, middle, c2, bottom, halfOffsetW,
					halfOffsetH, depth+1);

			// Bottom right
			offsetRay(r, 1, 0, halfOffsetW, halfOffsetH);
			c3 = doAntialiasingRecursive(r, middle, right, bottom, c3, halfOffsetW,
					halfOffsetH, depth+1);

			r.getDirection().set(backupDir);
		}

		return colorMedian(c0, c1, c2, c3);
	}

	Ray offsetRay(Ray r, int wsign, int hsign, Vector3d woff, Vector3d hoff) {
		r.getDirection().scaleAdd(wsign, woff);
		r.getDirection().scaleAdd(hsign, hoff);
		r.getDirection().normalize();
		cleanRay(r, r.getDirection());
		return r;
	}

	public Ray calcReflectRay(Ray ray) {
		// R = V - 2*(V.N) * N

		Vector3d N = ray.impactNormal;
		Vector3d newDir = new Vector3d(N);

		newDir.scale(-2 * N.dot(ray.getDirection()));
		newDir.add(ray.getDirection());
		newDir.normalize();

		// Con esto se evita intersectar nuevamente al mismo objeto
		ray.setImpactDist(ray.getImpactDist() - EPSILON);
		Vector3d intPoint = ray.getIntersectionPoint();
		ray.setImpactDist(ray.getImpactDist() + EPSILON);

		return new Ray(new Vector3d(intPoint), newDir, ray.getLastRenderable(),
				ray.getRIndex());
	}

	public Ray calcRefractRay(Ray ray, Renderable renderable) {

		// t = (n1/n2)* - ((n1/n2)CosI + sqrt(1-sinT^2))*N

		double n1 = ray.getRIndex();
		double n2 = renderable.getShader().getRefractionIndex();
		double n = n1 / n2;

		Vector3d N = new Vector3d(ray.impactNormal);
		Vector3d newDir = new Vector3d(ray.getDirection());

		double cosI = N.dot(ray.getDirection());
		double sinT2 = n * n * (1.0 - cosI * cosI);

		if (sinT2 > 1.0)
			return null;

		N.scale(n + Math.sqrt(1.0 - sinT2));
		newDir.scale(n);
		newDir.sub(N);

		newDir.normalize();

		ray.setImpactDist(ray.getImpactDist() + EPSILON);
		Vector3d intPoint = ray.getIntersectionPoint();
		ray.setImpactDist(ray.getImpactDist() - EPSILON);

		return new Ray(new Vector3d(intPoint), newDir, ray.getLastRenderable(),
				n2);

	}

	public Color getColor(Ray ray, int depth) {

		Color color = new Color(0, 0, 0);
		Renderable renderable;
		int shade;

		if (depth > MAX_DEPTH || ray == null
				|| (renderable = scene.intersect(ray)) == null
				|| scene.lightRenderables.size() == 0)
			return color;

		if (renderable.getShader().isLight())
			return ColorHelper.mulColor(ray.impactColor,
					((Primitive) renderable).getIntensity(renderable
							.getOrigin()));

		Vector3d Pn = ray.impactNormal;
		double refractionK = renderable.getShader().getRefraction();
		double reflectionK = renderable.getShader().getReflection();

		if (reflectionK > 0.0f) {
			Ray reflectRay = calcReflectRay(ray);
			color = ColorHelper.sumColor(color, ColorHelper.mulColor(getColor(
					reflectRay, depth + 1), reflectionK));
		}

		if (refractionK > 0.0f) {
			Ray refractRay = calcRefractRay(ray, renderable);
			if (refractRay != null)
				color = ColorHelper.sumColor(color, ColorHelper.mulColor(
						getColor(refractRay, depth + 1), refractionK));
		}

		// Iterate through light sources
		for (int i = 0; i < scene.lightRenderables.size(); i++) {

			shade = 1;

			Primitive l = (Primitive) scene.lightRenderables.get(i);

			Vector3d Ln = l.getNormal(ray.getSummarizedRay());

			// Creates a ray between light and primitive
			Ray r = new Ray(l.getOrigin(), Ln);

			// Raycast ray
			scene.intersect(r);

			Vector3d a = ray.getIntersectionPoint();
			Vector3d b = r.getIntersectionPoint();

			// Checks weather light ray impacts with prim or not to set shadow
			if (r.getLastRenderable() != null
					&& ray.getLastRenderable() == r.getLastRenderable()
					&& a.epsilonEquals(b, 0.3)) {
				shade = 0;
			}

			// Get ligth intensity between object and ligth source
			double intensity = l.getIntensity(renderable.getOrigin());

			// calculate diffuse shading
			double diffuse = ray.getLastRenderable().getShader().getDiffuse();

			if (intensity > 0) {

				Color primColor = ColorHelper.mulColor(ray.impactColor,1-refractionK);

				if (diffuse > 0 && shade != 1) {
					double dot = Ln.dot(Pn);

					if (dot > 0) {
						double diff = dot * diffuse * intensity;

						// add diffuse component

						color = ColorHelper.sumColor(color, ColorHelper
								.mulColor(ColorHelper.mulColor(primColor,
										primColor), diff));
					}

				}

				// determine specular component
				double specular = ray.getLastRenderable().getShader()
						.getSpecular();

				if (specular > 0 && shade != 1) {

					Vector3d V = new Vector3d(ray.getDirection());
					Vector3d R = new Vector3d(Ln);
					R.scale(-2.0 * Ln.dot(Pn));
					R.add(Pn);
					double dot = V.dot(R);

					if (dot > 0) {
						double spec = Math.pow(dot, 20) * specular;

						// add specular component
						color = ColorHelper.sumColor(color, ColorHelper
								.mulColor(primColor, spec));
					}

				}
				// add ilumination
				color = ColorHelper.sumColor(color, ColorHelper.mulColor(ilum(ray, l),1-refractionK));

				if (shade == 1) {
					color = ColorHelper.subColor(color, ColorHelper.mulColor(
							color, 0.45f * intensity));
				}
			}
		}

		return color;

	}

	public Color ilum(Ray ray, Primitive light) {
		return ColorHelper.mulColor(ray.impactColor, light.getIntensity(ray
				.getLastRenderable().getOrigin()));
	}

}